package org.mips.Sirius.ui;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;

import org.mips.R;
import org.mips.Sirius.components.Line;
import org.mips.Sirius.components.StopMonitoring;
import org.mips.Sirius.components.StopPoint;
import org.mips.Sirius.components.Vehicle;
import org.mips.Sirius.provider.ProviderException;
import org.mips.Sirius.provider.TransportInformationProvider;
import org.mips.Sirius.provider.SIRI.SIRIProvider;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;
import com.google.android.maps.OverlayItem;

/*
 * Main class
 * Implements the user interface and binds it with the needed components
 */
public class SiriusMap extends MapActivity implements LocationListener {

	/*
	 * Private fields
	 */

	// user interface
	private MapView map;
	private MapController mapController;
	public static final int DIALOG_FATAL_ERROR_ID = 1;
	public static final int DIALOG_NETWORK_ERROR_ID = 2;
	public static final int DIALOG_GPS_ERROR_ID = 3;
	public static final int DIALOG_LOADING = 4;
	public static final String MENU_FIND_ME = "Find me";
	public static final String MENU_TOGGLE_MAP = "Toggle satellite";
	private GeoPoint lastKnownLocation = null;
	private HashMap<String, Integer> linesColouring;
	private SiriusLoadingDialog loadingDialog;
	private SiriusLoadingDialog tapLoadingDialog;
	private SiriusStopMonitoringDialog smDialog;

	// components
	private LocationManager locationManager;
	private TransportInformationProvider provider;

	// default configuration
	private final int DEFAULT_ZOOM_LEVEL = 15;
	private final String providerURL = "http://mips42.altervista.org/etc/siri_feed.php";
	private final int requestDelay = 5000;

	// constants
	private final String LINES_DRAWN = "lines_drown";
	private final String STOPPOINTS_DRAWN = "stoppoints_drown";
	private final String VEHICLES_DRAWN = "vehicles_drown";

	/*
	 * Constructor
	 */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		linesColouring = new HashMap<String, Integer>();

		// create map
		map = createMap();
		if (map == null) {
			Log.d("debug", "creating map failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}

		// set up a default transport information source
		// FIXME: don't hardcode
		try {
			provider = new SIRIProvider(new URL(providerURL), requestDelay);
		} catch (MalformedURLException e) {
			Log.d("debug", "malformed URLs in config");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}
		if (provider == null) {
			Log.d("debug", "creating information provider failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}

		// create a location manager, a GPS listener and bind them
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		if (locationManager == null) {
			Log.d("debug", "creating location manager failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}

		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0,
				0, this);

		// add an overlay showing where the user is
		MyLocationOverlay userLocationOverlay = new FixedMyLocationOverlay(
				this, map);
		userLocationOverlay.enableMyLocation();
		userLocationOverlay.enableCompass();
		map.getOverlays().add(userLocationOverlay);

		Drawable defaultMarker;
		try {
			defaultMarker = MarkerFactory.createDefaultMarker();
		} catch (SiriusUIException e) {
			Log.d("debug", "no internet connectivity");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}

		// add an overlay showing the transit map (lines)
		LinesMapOverlay linesOverlay = new LinesMapOverlay(defaultMarker);
		if (linesOverlay == null) {
			Log.d("debug", "creating lines overlay failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}
		map.getOverlays().add(linesOverlay); // place it on top of the map

		// add an overlay showing where buses are
		VehicleMapOverlay busOverlay = new VehicleMapOverlay(defaultMarker);
		if (busOverlay == null) {
			Log.d("debug", "creating bus overlay failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}
		map.getOverlays().add(busOverlay); // place it on top of the map

		// add an overlay showing where stops are
		StopPointsMapOverlay stopOverlay = new StopPointsMapOverlay(
				defaultMarker);
		if (stopOverlay == null) {
			Log.d("debug", "creating stop overlay failed");
			showDialog(DIALOG_FATAL_ERROR_ID);
			return;
		}
		map.getOverlays().add(stopOverlay); // place it on top of the map

		// set map as current view
		setContentView(map);

		// prepare dialogs
		ArrayList<String> tokens = new ArrayList<String>();
		tokens.add(LINES_DRAWN);
		tokens.add(STOPPOINTS_DRAWN);
		tokens.add(VEHICLES_DRAWN);
		loadingDialog = new SiriusLoadingDialog(this, tokens);

		smDialog = new SiriusStopMonitoringDialog(this);

		loadingDialog.show();

	}

	/*
	 * Creates the map, i.e. the main user interface
	 */
	private MapView createMap() {
		// FIXME: generate "release" API key
		MapView map = new MapView(this, getString(R.string.maps_api_key));
		mapController = map.getController();
		mapController.setZoom(DEFAULT_ZOOM_LEVEL); // set a default zoom level
		map.setSatellite(true);
		map.setClickable(true);
		map.setBuiltInZoomControls(true); // allow user to change zoom level

		return map;
	}

	/*
	 * Called the first time a certain dialog is called
	 */
	@Override
	protected Dialog onCreateDialog(int dialogId) {
		Dialog dialog;
		AlertDialog.Builder builder = new AlertDialog.Builder(this);
		builder.setCancelable(false);

		switch (dialogId) {

		case DIALOG_FATAL_ERROR_ID:

			builder.setMessage("Sorry, I couldn't boot and I'm dying.");

			builder.setNeutralButton("Ok",
					new DialogInterface.OnClickListener() {
						public void onClick(DialogInterface dialog, int id) {
							finish();
						}
					});
			dialog = builder.create();
			break;
		case DIALOG_NETWORK_ERROR_ID:

			builder
					.setMessage("A network error has occured. Please try again.");
			builder.setNeutralButton("Ok",
					new DialogInterface.OnClickListener() {
						public void onClick(DialogInterface dialog, int id) {
							dialog.dismiss();
						}
					});
			dialog = builder.create();
			break;
		case DIALOG_GPS_ERROR_ID:

			builder.setMessage("Your current location is not yet available");
			builder.setNeutralButton("Ok",
					new DialogInterface.OnClickListener() {
						public void onClick(DialogInterface dialog, int id) {
							dialog.dismiss();
						}
					});
			dialog = builder.create();
			break;
		case DIALOG_LOADING:

			SiriusLoadingDialog sldialog = new SiriusLoadingDialog(this);
			sldialog.setMessage("Loading");
			dialog = sldialog;
			break;
		default:
			dialog = null;
			Log.d("debug", "unknown dialog requested");
			break;
		}

		if (dialog == null) {
			Log.d("debug", "building alert dialog failed");
		}

		return dialog;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		menu.add(MENU_FIND_ME);
		menu.add(MENU_TOGGLE_MAP);
		return true;
	}

	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		if (item.getTitle() == MENU_FIND_ME) {
			if (lastKnownLocation == null) {
				showDialog(DIALOG_GPS_ERROR_ID);
			} else {
				map.getController().setCenter(lastKnownLocation);
			}
		} else if (item.getTitle() == MENU_TOGGLE_MAP) {
			map.setSatellite(!map.isSatellite());
		}
		return true;
	}

	/*
	 * Tell the Maps API we're not using route information (yet)
	 */
	@Override
	protected boolean isRouteDisplayed() {
		return false;
	}

	private class VehicleMapOverlay extends SiriusOverlay {

		public VehicleMapOverlay(Drawable defaultMarker) {
			super(defaultMarker);
			this.populate();
		}

		@Override
		public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
				long when) {
			super.draw(canvas, mapView, shadow);

			synchronized (provider) {
				ArrayList<Vehicle> vehicles;

				try {
					vehicles = provider.getRealtimeVehicleInformation();
				} catch (ProviderException pe) {
					Log.d("debug",
							"Failed to retrieve real-time vehicle information"
									+ pe.getCause().getMessage());
					return true;
				}

				if ((vehicles == null) || linesColouring.isEmpty())
					return true;

				this.items = new ArrayList<OverlayItem>();

				// draw markers for each vehicle
				for (Vehicle vehicle : vehicles) {
					OverlayItem item = new OverlayItem(
							vehicle.getCoordinates(), "", "");

					// get a marker for the vehicle
					Drawable marker;
					try {
						marker = MarkerFactory.createVehicleMarker(vehicle
								.getName(), Colouring
								.getHTMLColour(linesColouring.get(vehicle
										.getLineReference())));
					} catch (SiriusUIException e) {
						Log.d("debug", "no internet connectivity");
						showDialog(DIALOG_FATAL_ERROR_ID);
						return false;
					}
					ItemizedOverlay.boundCenterBottom(marker);
					item.setMarker(marker);
					this.items.add(item);
				}

				this.populate();

				loadingDialog.notifyReady(VEHICLES_DRAWN);

				return true;
			}
		}
	}

	private class StopPointsMapOverlay extends SiriusOverlay {

		public StopPointsMapOverlay(Drawable defaultMarker) {
			super(defaultMarker);
			this.populate();
		}

		@Override
		public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
				long when) {
			super.draw(canvas, mapView, shadow);

			synchronized (provider) {
				ArrayList<StopPoint> stopPoints;

				try {
					stopPoints = provider.getStaticStopPointsInformation();
				} catch (ProviderException pe) {
					Log.d("debug", "Failed to retrieve stops information");
					return true;
				}

				if (stopPoints == null)
					return true;

				this.items = new ArrayList<OverlayItem>();
				// draw markers for each stop
				for (StopPoint stop : stopPoints) {
					OverlayItem item = new SiriusOverlayItem(stop, stop
							.getCoordinates(), stop.getName(), stop
							.getReference());
					// get a marker for the stop
					Drawable marker;
					try {
						marker = MarkerFactory.createStopMarker(stop
								.getReference());
					} catch (SiriusUIException e) {
						Log.d("debug", "no internet connectivity");
						showDialog(DIALOG_FATAL_ERROR_ID);
						return false;
					}
					ItemizedOverlay.boundCenterBottom(marker);
					item.setMarker(marker);
					this.items.add(item);
				}

				this.populate();

				loadingDialog.notifyReady(STOPPOINTS_DRAWN);

				return false;
			}
		}

		@Override
		public boolean onTap(final int x) {
			SiriusOverlayItem tappedItem = (SiriusOverlayItem) this
					.createItem(x);
			if (tappedItem == null) {
				Log.d("tap", "Tapped item with index " + x + " not found");
				return false;
			}

			Log.d("tap", "Tapped item with index " + x + ": "
					+ tappedItem.getTitle());

			tapLoadingDialog = new SiriusLoadingDialog(SiriusMap.this);
			tapLoadingDialog.show();

			handler.postDelayed(new Runnable() {

				@Override
				public void run() {

					SiriusOverlayItem tappedItem = (SiriusOverlayItem) StopPointsMapOverlay.this
							.createItem(x);

					smDialog.setDismissable(tapLoadingDialog);

					try {
						StopMonitoring stopMonitoring = provider
								.getRealtimeStopPointInformation(tappedItem
										.getStop());
						tappedItem.getStop().setLastMonitoring(stopMonitoring);

						smDialog.setStop(tappedItem.getStop());

					} catch (ProviderException pe) {
						Log.d("tap",
								"Failed to retrieve stop monitoring information for stop "
										+ tappedItem.getStop());
						smDialog.setStop(null);
						handler.sendMessage(Message.obtain(handler, 0,
								DIALOG_NETWORK_ERROR_ID, 0));
					}

				}
			}, 500);

			return true;
		}
	}

	private class LinesMapOverlay extends SiriusOverlay {

		public LinesMapOverlay(Drawable defaultMarker) {
			super(defaultMarker);
			this.populate();
		}

		@Override
		public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
				long when) {
			super.draw(canvas, mapView, shadow);

			synchronized (provider) {
				ArrayList<Line> lines;

				try {
					lines = provider.getStaticLinesInformation();
				} catch (ProviderException pe) {
					Log.d("debug", "Failed to retrieve lines information");
					return true;
				}

				if (lines == null)
					return true;

				this.items = new ArrayList<OverlayItem>();

				for (Line line : lines) {

					linesColouring.put(line.getReference(), new Integer(
							Colouring.getColour(lines.indexOf(line), lines
									.size())));
					Paint paint = new Paint();
					paint.setColor(linesColouring.get(line.getReference()));
					paint.setAlpha(80);
					paint.setStrokeWidth(5f);
					paint.setStyle(Paint.Style.STROKE);
					paint.setStrokeJoin(Paint.Join.ROUND);

					Path path = new Path();

					Point oldPoint = null;
					// draw the line map
					for (GeoPoint wayPoint : line.getStopPointsPath()) {
						Point point = new Point();
						mapView.getProjection().toPixels(wayPoint, point);

						if (oldPoint != null) {
							path.lineTo(point.x, point.y);
							oldPoint = point;
						} else {
							path.moveTo(point.x, point.y);
							oldPoint = point;
						}
					}

					canvas.drawPath(path, paint);
				}

				loadingDialog.notifyReady(LINES_DRAWN);

				return false;
			}
		}
	}

	/*
	 * Called whenever the GPS position of the user changes
	 */
	public synchronized void onLocationChanged(Location newLocation) {

		Log.d("debug", "Location is now: " + newLocation.getLatitude() + " "
				+ newLocation.getLongitude());

		int newLatitude = (int) (newLocation.getLatitude() * 1000000);
		int newLongitude = (int) (newLocation.getLongitude() * 1000000);
		GeoPoint newLocationPoint = new GeoPoint(newLatitude, newLongitude);

		if (newLocationPoint != lastKnownLocation) {
			lastKnownLocation = newLocationPoint;

			Log.d("debug", "Centering on: " + newLocationPoint.getLatitudeE6()
					+ " " + newLocationPoint.getLongitudeE6());
		}

	}

	@Override
	public void onProviderDisabled(String arg0) {
	}

	@Override
	public void onProviderEnabled(String provider) {
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
	}

	private Handler handler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			Log.d("tap", "handler " + msg.obj);

			SiriusMap.this.showDialog(msg.arg1);
		}
	};
}